package com.course.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Constructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Data
@NoArgsConstructor
@Component
public class RedisUtils {

    @Resource
    StringRedisTemplate stringRedisTemplate;


    public static final String LOCK_KEY = "lock:";
    public void set(String key, Object value){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),1L,TimeUnit.HOURS);
    }
    public void set(String key, Object value, long time, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);
    }
    /**
     * 设置逻辑过期 的数据
     */
    public void setWithExpire(String key, Object value, long time, TimeUnit timeUnit){
        // 对value进行 逻辑过期字段增强
        RedisExpireData expireData = new RedisExpireData(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)), value);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(expireData));
    }

    public String get(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    public <T> T get(String key,Class<T> tClass){
        String result = stringRedisTemplate.opsForValue().get(key);
        return JSONUtil.toBean(result,tClass);
    }

    /**
     * 从Redis中获取带有过期时间的数据，并根据过期状态执行相应操作
     *
     * @param key 数据的键
     * @param tClass 数据的类类型，用于反序列化数据
     * @param <T> 泛型参数，表示数据的类型
     * @return 返回反序列化后的数据对象
     */
    public <T> T getWithExpire(String key, Class<T> tClass){
        // 从Redis中获取数据字符串
        String result = stringRedisTemplate.opsForValue().get(key);
        // 将字符串反序列化为带有过期时间的数据对象
        RedisExpireData redisExpireData = JSONUtil.toBean(result, RedisExpireData.class);
        // 获取数据部分
        JSONObject data = (JSONObject) redisExpireData.getData();
        // 将数据反序列化为指定类型的对象
        return JSONUtil.toBean(data, tClass);
    }

    /**
     * 可解决缓存穿透问题
     * 1：从Redis中获取指定key的值。
     * 2：如果值存在，直接返回反序列化后的对象。
     * 3：如果值不存在，调用supplier获取数据。
     * 4：如果supplier结果也无数据，则缓存一个空对象5分钟，防止缓存穿透。
     * 5：如果supplier结果有数据，将数据写入Redis并返回
     */
    @SneakyThrows
    public <T> T getPenetration(String key, Class<T> tClass, long time, TimeUnit timeUnit , Supplier<T> supplier){
        String result = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(result)){    // 当前数据存在 直接返回
            return JSONUtil.toBean(result,tClass);
        }
        // 重建缓存
        T bean = supplier.get();    // 具体可为查询数据库操作
        // 数据库也不存在 则缓存空对象
        if (bean == null){
            // 反射创建空对象存储即可
            Constructor<T> constructor = tClass.getConstructor();
            T instance = constructor.newInstance();
            this.set(key,instance,5,TimeUnit.MINUTES);
        }else {
            // 数据写入Redis
            this.set(key,bean,time,timeUnit);
        }
        return bean;
    }


    /**
     * 获取带有分布式锁保护的数据，防止缓存击穿。
     * @param key 键名，用于在Redis中查找缓存数据
     * @param tClass 数据类型，用于反序列化返回的数据
     * @param time 缓存过期时间
     * @param timeUnit 时间单位，指定过期时间的单位
     * @param supplier 数据提供者，当缓存中无数据时调用此函数获取新数据，缓存重建操作
     * @return 返回从缓存或supplier中获取的数据对象
     */
    public <T> T getBreakDownLock(String key, Class<T> tClass, long time, TimeUnit timeUnit ,Supplier<T> supplier){
        String result = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(result)){    // 当前数据存在 直接返回
            return JSONUtil.toBean(result,tClass);
        }
        // 获取互斥锁
        String lockKey = LOCK_KEY + key;    // 互斥锁key
        try {
            boolean tryLock = this.tryLock(lockKey);
            if (!tryLock){  // 未获取到锁 线程进行等待重试
                Thread.sleep(50);   // 休眠50ms后重试
                return getBreakDownLock(key,tClass,time,timeUnit,supplier); // 递归重试
            }
            // 获取到锁
            // 缓存重建
            T bean = supplier.get();
            // 写入缓存
            this.set(key,bean,time,timeUnit);
            // 返回数据
            return bean;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 释放锁
            this.unLock(lockKey);
        }
    }
    /**
     * 获取并更新带有过期概念的缓存数据
     * 当缓存数据过期时，使用互斥锁确保只有一个线程更新缓存，其他线程继续使用旧数据
     * @param key 缓存的唯一键
     * @param tClass 缓存数据的类型
     * @param time 缓存有效期
     * @param timeUnit 缓存有效期的时间单位
     * @param supplier 数据提供者，当缓存中数据已过期时调用此函数获取新数据，缓存重建操作
     * @param <T> 泛型参数，表示缓存数据的类型
     * @return 返回缓存的数据，如果缓存过期则返回旧数据或新数据
     */
    public <T> T getBreakDownExpireLock(String key, Class<T> tClass, long time, TimeUnit timeUnit ,Supplier<T> supplier){
        String result = stringRedisTemplate.opsForValue().get(key);
        RedisExpireData redisExpireData = JSONUtil.toBean(result, RedisExpireData.class);
        T bean = JSONUtil.toBean((JSONObject) redisExpireData.data, tClass);    // 具体的数据
        // 校验数据是否过期
        if (redisExpireData.isExpire()){    // 未过期 直接返回即可
            return bean;
        }
        // 获取互斥锁
        String lockKey = LOCK_KEY + key;    // 互斥锁key
        boolean tryLock = this.tryLock(lockKey);
        if (!tryLock){  // 未得到锁
            return bean;   // 直接返回旧数据
        }
        // 获取到锁
        CompletableFuture.runAsync(() -> {
            try {
                // 重建缓存
                T t = supplier.get();
                this.set(key,t,time,timeUnit);
            } finally {
                // 释放锁
                unLock(lockKey);
            }
        });
        return bean;
    }


    public void del(String key){
        stringRedisTemplate.delete(key);
    }

    // 获取锁
    private boolean tryLock(String key){
        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
        return BooleanUtil.isTrue(ifAbsent);
    }
    // 释放锁
    private void unLock(String key){
        this.del(key);
    }
    /**
     * 逻辑过期字段 增强类
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class RedisExpireData{
        private LocalDateTime expireTime;
        private Object data;

        public boolean isExpire(){
            return expireTime.isAfter(LocalDateTime.now());
        }
    }

}
